\chapter{面向网络协同应用编程的编程框架研究}
\label{cha:socker}
本章的主要研究网络协同应用编程框架，其目标是基于验证平面的可靠验证结果实现网络与应用协同编程。首先分析了当前网络应用编程中的“网络与应用割裂”现象，进而提出了实现网络协同应用编程所需克服的挑战。为了克服这些挑战，创新性地提出基于Socket跟踪的网络协同应用编程框架Socker，Socker提供了声明式的网络查询语言NQL（Network Query Language）和编程式的接口，同时还整合了基于ALTO和perfSONAR的网络路径可见性框架。实验结果证明，与传统套接字编程相比，Socker能够提升20\%的数据传输速度，同时只引入了轻微的时间和内存开销。


\cref{fig:nai-outline}列出了面向网络协同应用编程的编程框架研究路径。首先在\cref{sec:nai-background}中介绍了面向网络协同应用编程的编程框架研究背景，在\cref{sec:nai-problem}中，提出网络协同应用编程框架所研究的问题和直观思路，在\cref{sec:nai-challenge}中，提出解决问题所面临的挑战，在\cref{sec:nai-solution}和\cref{sec:nai-solution-vis}中针对挑战提出相应的解决方案，并在\cref{sec:nai-eval}中对所提出的解决方案进行实验验证。

\begin{figure}
  \centering
  \includegraphics[width=\linewidth]{figures/nai/outline.pdf}
  \caption{面向网络协同应用编程的编程框架研究路径}
  \label{fig:nai-outline}
\end{figure}

\section{面向网络协同应用编程的编程框架研究背景}
\label{sec:nai-background}
为了实现可验证路由编程的应用平面，需要构建相应的编程模型和接口将应用逻辑与网络控制相结合。同时，为了获取可靠的网络状态，可验证路由编程必须依赖于验证平面提供的可靠网络信息。因此，为确保网络协同应用编程对网络的控制保持高可靠性和高适应性，实现相应的编程框架是必不可少的。网络协同应用编程通过在编程角度将网络控制和应用逻辑结合，能够实现完全用户定义的网络和应用协同过程，而对于编程系统来说，系统需要提供相应的编程模型和编程接口，基于网络验证系统向应用提供可靠的网络路由信息，实现一定程度的网络“白盒”化，以实现可验证的网络路由编程。

% 当前，新兴的应用程序和网络对应用程序和网络之间的双向感知提出了越来越多的要求，传统的网络黑盒模型已经无法满足应用的高性能需求，如何将应用程序和网络整合起来已经得到了广泛的研究。因此，在过去几年中，应用程序和网络控制的耦合已经变得越来越普遍。一方面，工程人员已经做出了一定的尝试，使网络了解应用程序的需求。例如，谷歌的SDN网络~\cite{jain2013b4, yap2017taking}已经实现了收集应用程序数据并在数据传输上做TE（Traffic Engineering）决策。另一方面，某些应用程序还具有自适应网络状态的技术，例如，基于DASH的应用程序~\cite{sodagar2011mpeg}根据当前带宽调整其比特率。在云游戏~\cite{zhang2020mowie}中，采用自适应算法实现基于单向延迟的优化。此外，还有研究将应用程序和网络控制闭环结合起来。例如，在Minerva~\cite{nathan2019end}中，拥塞控制算法基于用户QoE，而速率选择则根据网络带宽。

\textbf{网络应用编程中的“网络与应用割裂”现象。} 在当前的网络架构中，网络的设计和路由规划通常独立于应用。在应用部署之前，网络已按照一套固定的规则运行。网络设计者主要关注设备之间的连接性、带宽是否足够以及一些安全性问题，而并未考虑网络要承载的具体应用。同时，路由协议如OSPF等只考虑网络链路的状态来构建路由，对网络应用并不敏感。比如，在一个典型的三层（接入层、汇聚层、核心层）企业网络中，网络工程师可能会配置汇聚层和核心层运行OSPF动态路由协议，接入层配置VLAN和VLANIF\footnote{以华为系列设备配置为例}网关接口，并将VLANIF的IP前缀分发到OSPF，确保不同部门的计算机能够相互通信，而并不关心终端计算机上运行的具体应用。

另一方面，在进行网络应用编写时，开发人员可能只是简单地通过套接字指定要连接到的IP地址和端口，而不清楚数据包是如何穿越网络并到达目标计算机的。这种割裂导致了网络和应用之间缺乏有效的沟通和协同。网络设计和应用开发是两个相对孤立的过程，缺乏一种机制使它们能够更紧密地协同工作，以更好地适应多样化的应用需求。


% \textbf{网络应用编程中的数据包失控（Packet out of control）现象。}
具体而言，数据包是网络和应用之间主要的连接桥梁，应用负责产生数据包，而网络负责转发数据包。然而，目前的应用编程主要依赖于套接字（Socket）编程模型，套接字抽象简化了网络编程，但也带来了一定的可控性问题。如图~\ref{fig:nai-old-code} 所示为一个网络传输应用的Python代码示例，在这个示例中，操作系统负责处理网络相关操作，例如建立连接、发送和接收数据。一旦数据通过send函数发送给操作系统，应用程序就失去了对数据的直接控制。这种套接字编程模型的不足之处在于，应用程序无法在数据包传输的过程中实时地对数据进行处理或干预。此外，应用程序通常无法深入了解网络层面的细节，如路由选择和拓扑结构。因此，为了提高网络应用的可控性和适应性，需要一种更加灵活的网络编程方法和框架，允许应用程序更主动地管理和操作网络对数据包的处理过程，以便更好地满足不同应用场景的需求。所以，本文进行了面向网络协同应用编程的编程框架研究。

% 为了实现网络和应用的相互感知，实现一定程度的网络“白盒”化，基于SDN的网络架构提供了网络控制的API，使得应用程序可以通过SDN控制器的北向接口（Northbound API）对网络进行控制，而当前的北向接口设计往往是不可编程的声明式接口，仅适用于某一种类型的应用程序或某一种类型的网络控制，缺乏一个联合编程应用程序和网络控制的通用框架，限制了网络和应用结合的灵活性。基于SDN架构，可以进一步增强对网络和应用的耦合，在编程角度将网络与应用结合起来，从而实现更灵活的可编程控制策略，网络协同应用编程能够在编程时将网络控制和应用逻辑进行结合，从而让应用程序根据网络状态决定如何处理数据流。如图~\ref{fig:nai-code} 所示为一种网络协同应用编程示例，程序可以读取网络状态（如拓扑和带宽信息），应用程序根据需求计算路由路径，并在发送数据时指定使用的路径。

\begin{figure}[ht]
\begin{python}
  import socket

  # 创建套接字
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  # 连接到远程服务器
  s.connect((IP, PORT))
  # 发送数据
  s.send(DATA)
  # 关闭套接字
  s.close()
\end{python}
\caption{传统Socket网络编程Python代码示例}
\label{fig:nai-old-code}
\end{figure}


% 这将带来一个痛苦的困境，因为小草图会导致信息的丢失，合作效率将降低，而大草图可能会导致制作草图和传递草图的巨大开销，以及暴露过多的信息，可能会引起双方的安全问题。此外，它们仅适用于某一种类型的应用程序或某一种类型的网络控制。缺乏一个联合编程应用程序和网络控制的通用框架。随着网络技术的不断发展，如何在不牺牲信息完整性和安全性的前提下，实现应用程序和网络控制之间更加紧密的协作成为了一个重要的问题。有必要研究出通用框架，实现应用程序和网络控制的联合编程，使其可以在更广泛的应用场景中发挥作用。


% NF可以作为其相应AF与外部网络通信网络策略的中间点，同时保持AF的应用程序细节。
% 最后，该提议提供了一个统一的框架，用于在分布式系统内部同时处理AF和NF。当网络作为服务变得流行时，这种方法有助于应用程序用户更好地理解和管理整个系统。

% \begin{figure}
%   \centering
%   \includegraphics[width=0.7\linewidth]{figures/nai-example.pdf}
%   \caption{NAI架构示例}
%   \label{fig:nai-example}
% \end{figure}
% \begin{figure}
%   \centering
%   \includegraphics[width=0.7\linewidth]{figures/nai-example-code.png}
%   \caption{具有动态gzip压缩级别的文件上传程序的代码片段}
%   \label{fig:nai-example-code}
% \end{figure}

% 本文用一个简单的例子展示将AF和NF定位在一起的必要性。如图~\ref{fig:nai-example}所示，客户端运行一个上传一定数量的文件到服务器的应用程序。它们通过一个只有3个交换机节点的小型网络连接。路径s1-s2具有较短的延迟但较小的带宽，而s1-s3-s2则相反。AF在这里是选择一个压缩级别来压缩文件。级别越高，传输的文件就越小，当然，压缩时间也会更长。NF在这里是选择一条从客户端到服务器的路径来上传文件。目标是尽快完成上传。

% 上传文件的总时间由压缩时间和传输时间组成。较低的压缩级别意味着较短的压缩时间，但在传输时文件大小更大。此外，网络路径的带宽和延迟也会影响传输时间。因此，AF和NF应该能狗合作完成最终目标，从而决定压缩级别和路径。这也说明了为什么将AF和NF放在同一主机上的上下文中会有益处。在这种情况下，它们可以直接交换信息，优化整个传输过程并取得更好的性能。

% 在图~\ref{fig:nai-example}中，当AF和NF分开运行时（如红色箭头所示），它们可能无法有效地交换信息。在这种情况下，如果没有拥塞，理想情况下，AF将选择较低的压缩级别，而NF将选择s1-s2路径以加快上传速度。但是，如果AF突然开始发送一个大文件，而没有及时通知NF切换路径，s1-s2将很快被过载。另一方面，如果s1-s3-s2是多个AF的备用路径，而它们所有的短路径突然出现拥塞，NF将重新路由所有流量通过s1-s3-s2。如果没有通知AF提高压缩级别以减少发送速率，s1-s3-s2将很快收到大量流量的积累并且也会被拥塞。为了解决这样的问题，用户应编写一个额外的协议来在AF和NF之间交换信息，并重写其代码以插入该协议，这是非常麻烦的。

% 与此相反，如果在端点主机上将AF和NF集成在同一个上下文中，如图~\ref{fig:nai-example}中的绿色箭头所示。具体而言，网络控制程序（NF）被移动到端点主机上，AF和NF可以快速响应彼此的任何状态变化，因此端点主机成为“控制器”，但只能控制其自身发起的流。如图~\ref{fig:nai-program-code}所示，只需要在NF和AF中添加一些带有可共享变量的注释即可。AF根据网络状态确定文件压缩级别，NF根据来自AF状态的文件类型计算路由路径（\textit{maxBWPath}或\textit{shortestPath}）。这种方法简单易用，无需修改原始代码或添加额外的通信协议。同时，这也是提高系统可扩展性的一种有效方法，因为当需要添加新功能或更改系统行为时，只需修改相应的AF或NF即可，而无需对整个系统进行重大更改。

% 在现实情况下，不同的应用程序可能需要不同的AF和NF策略，例如，在视频流传输中，AF可能是对视频进行编码，而NF可能是选择多条路径来保证视频传输的稳定性。因此，将AF和NF定位在一起，可以允许它们直接交换信息并更好地协同工作，从而提高整体性能。

\section{面向网络协同应用编程的编程框架问题描述}
\label{sec:nai-problem}

网络协同应用编程的目标是将网络编程与应用编程融为一体，使得网络可以根据应用需求调整其路由策略，而应用则可以根据网络状态灵活调整其执行逻辑。这一融合是实现网络与应用相互协同的关键手段之一。为实现这一目标，至关重要的是提供一套易于使用的编程框架，以实现对数据传输的灵活控制。本节通过实际案例分析，探讨构建面向网络协同应用编程的编程框架所面临的问题。

\textbf{P1: 如何实现数据传输需求与路径的适配。}以图~\ref{fig:nai-motivating-case} 所示的网络为例，该网络包含三个交换机和两个主机，物理网络拓扑中从Client到Server存在两条路径，其中ShortestPath为最短路径，MaxBWPath为最大带宽路径。应用程序有两种类型的数据从Client发往Server：一种为实时消息数据，应用希望通过最短路径ShortestPath进行传输；另一种为文件传输数据，应用希望通过最大带宽路径MaxBWPath进行传输。

传统的网络与应用分离的编程模型无法实现上述目标，在该模型下，网络控制完全独立于应用，网络路由往往在应用运行之前就基于某种路由协议（如OSPF，ISIS，和BGP协议等）全部配置完毕，导致Client到Server的路径无法根据应用需求进行动态调整。另一方面，在应用程序的角度，传统编程模型如基于Socket的传统网络编程模型只能控制数据包在终端设备上的处理过程，如图~\ref{fig:old-code} 所示为Client端的Python代码示例，操作系统将网络相关操作封装为Socket函数调用，程序员只能控制应用产生的数据包头，无法控制其在网络的传输路径。已有工作如Socket Intents~\cite{schmidt2013socket} 在该案例中也无法实现不同路径传输，因为Client只有一个接入网络的端口，因此无法通过切换端口实现路径切换。所以，如何实现数据传输需求与路径的适配是面向网络协同应用编程的编程框架需要解决的关键问题之一。

\begin{figure}[t]
  \centering
  \includegraphics[width=.9\linewidth]{figures/nai/nai-motivating-case.pdf}
  \caption{具有两条不同传输路径的示例网络}
  \label{fig:nai-motivating-case}
\end{figure}

\begin{figure}[t]
\begin{python}
  from socket import socket
  
  sock = socket(AF_INET, SOCK_STREAM)
  dst = ("10.0.0.2", 80)
  sock.connect(dst)

  def send_realtime(data):
    sock.send(data)
  
  def send_file(file)
    sock.send(file)
\end{python}
\caption{传统网络编程发送不同类型数据示例}
\label{fig:old-code}
\end{figure}

% \begin{figure}[t]
% \begin{python}
%   from socket import socket
%   from network import topo
  
%   sock = socket(AF_INET, SOCK_DGRAM)
%   sock.connect("10.0.0.2", 80)

%   def send_realtime(data):
%     sock.sendWithPath(data, topo.shortestPath(Client, Server))
  
%   def send_file(file)
%     sock.sendWithPath(file, topo.maxBWPath(Client, Server))
% \end{python}
% \caption{网络与应用结合编程Python代码示例}
% \label{fig:nai-code}
% \end{figure}
为了解决网络协同应用编程中的路径适配和感知，需要在编程角度将网络控制和应用编程进行耦合，并提供相应的网络状态获取接口，具体而言，实现面向网络协同应用编程的编程框架的直观思路如下：

\textbf{I1: 将网络控制耦合到应用编程。}传统网络和应用之间存在明显的边界，应用程序对网络的控制能力有限。然而，在面向网络协同应用编程的理念下，在编程角度将二者耦合可以打破这种分隔，使得应用程序能够更积极地参与到网络控制的过程中。一种直接有效的方式是通过提供相应的库函数来扩展传统网络编程的功能，例如，针对\cref{fig:nai-motivating-case}中所示的案例，\cref{fig:nai-intuition-code}所示的示例代码中引入了sendByPath函数，允许开发者明确指定数据传输的路径，编程框架利用图算法（如shortestPath和weightedShortestPath）在网络拓扑上计算最优路径，并将该路径与内核数据包进行绑定，实现了数据传输需求与路径的精确匹配。这种直观的编程方式为开发者提供了控制网络行为的能力，从而实现数据传输需求与路径的适配。


\begin{figure}[ht]
  \begin{python}
from socket import socket
from NAI import topo, shortestPath, weightedShortestPath

sock = socket(AF_INET, SOCK_STREAM)
dst = ("10.0.0.2", 80)
sock.connect(dst)

def send_realtime(data):
  sp = shortestPath(topo, src=CLIENT, dst=SERVER)
  sock.sendByPath(data, sp)

def send_file(file)
  maxBwPath = weightedShortestPath(topo, src=CLIENT, dst=SERVER)
  sock.sendByPath(file, maxBwPath)
  \end{python}
  \caption{将网络控制耦合到应用编程代码示例}
  \label{fig:nai-intuition-code}
\end{figure}


\textbf{P2: 如何根据网络状态调整应用逻辑。}在\cref{fig:nai-problem-visibility}中的案例中，Client的目标是将大小为$Size$的缓存数据写入Memcached集群。与第$i$个Memcached服务器的通信具有带宽$BW_i$和双向延迟$RTT_i$。为了最大程度地提高数据写入速度，Client希望根据网络状况灵活调整其目标服务器的选择策略，以便选择最佳的Memcached服务器作为写入目标。定义$o = \arg\min_i Size/BW_i + RTT_i$，表示第$o$个服务器为最佳服务器。此时，Client将调整其TCP缓冲区大小为$BW_o \times RTT_o$，以优化数据传输性能。在目前的“网络与应用割裂”网络架构下，实现上述目标变得困难。网络的“黑盒”特性使得应用无法准确获取路径指标，例如带宽和延迟。如果在应用内通过实时测量网络路径状态则会浪费大量的带宽资源，因此，通过设计一个框架，使应用能够通过该框架统一地获取关键的网络信息，且需要保证获取信息的可靠性，并基于这些信息调整其执行逻辑，以使应用能够更主动地响应网络状况，如何根据网络状态调整应用逻辑成为面向网络协同应用编程的编程框架亟需解决的关键问题之一。


\begin{figure}
  \centering
  \includegraphics[width=0.7\linewidth]{figures/nai/problem-visibility.pdf}
  \caption{根据网络状态调整应用逻辑示例}
  \label{fig:nai-problem-visibility}
\end{figure}


\textbf{I2: 在应用内测量路径状态。}通过在应用内进行路径状态的测量，应用程序能够更全面地了解网络的性能和状况，为实现更智能的网络适应性提供基础。一种常见的方法是在应用内嵌入测量模块，该模块负责定期或根据需要发送测量数据以评估网络路径的性能。具体而言，在\cref{fig:nai-intuition-code1}的示例代码中提供了一种针对问题P2中\cref{fig:nai-problem-visibility}所示案例的方案，代码引入一个名为measurePath的函数，该函数负责在应用内测量特定路径的状态。应用程序通过发送探测数据包，并在收到响应时计算路径的带宽、延迟等性能指标，然后选择最佳的Memcached服务器，并设置合适的TCP缓冲区大小。通过在应用内进行路径状态测量，应用程序可以动态地感知网络环境的变化，并相应地调整其行为，从而实现根据网络状态调整应用逻辑。


\begin{figure}[ht]
  \begin{python}
from memcached import MemcachedCli
from NAI import argmin, measurePath

TARGETS = [MEMCACHE1, MEMCACHE2, MEMCACHE3]
bw = {}
rtt = {}

for server in TARGETS:
  bw[server], rtt[server] = measurePath(CLIENT, server)

target = argmin(srv, TARGETS, lambda srv: sizeof(DATA) / bw[srv] + rtt[srv])
buf_size = bw[target] * rtt[target]
memcached_client = MemcachedCli(target, buffer_size=buf_size)
memcached_client.send(DATA)
  \end{python}
  \caption{在应用内测量路径状态代码示例}
  \label{fig:nai-intuition-code1}
\end{figure}


% 以图~\ref{fig:nai-motivating-case} 为例，Client端包含两个AF，发往Server的


% \textbf{P1：如何区分不同类型的数据流。} 应用程序逻辑本身可以很容易通过相关变量区分数据类型，然而，当数据进入内核态并封装为数据包时，相关变量信息全部丢失，比如在Socket编程模型中，内核态只保留了Socket创建过程中的元数据信息（如目的IP，端口号等），需要将数据的类型信息传递到内核态封装数据包的过程中，从而区分发往网络中的数据包类型。

% \textbf{P1: 如何将不同类型的数据流与网络路径绑定。} 对于不同类型的数据流，需要有相应的接口将数据流与网络路径进行绑定，编程框架需要有相应的接口来设置网络路径并使得该路径，并使对应类型的数据流通过该路径进行正确转发。

% \textbf{P2: 如何获取网络的实时状态。}应用程序需要能够根据网络状态调整自身逻辑，比如在带宽较低的场景下应用程序开源选择用较高压缩率的压缩方法对数据进行压缩后再发送，以及应用程序根据网络的带宽和延迟设置TCP缓冲区的大小，这就需要应用程序在未发送数据前就能够获取到相关网络状态信息。
% \textbf{P3: 如何在网络或应用状态变化时自动触发AF和NF的重新执行。}由于网络和应用状态随时可能发生变化，AF和NF所依赖的状态发生变化时，系统需要能够自动触发AF和NF的重新执行。
  % \item 网络如何根据应用需求生成数据平面配置

% \section{面向网络协同应用编程的编程框架的基本思路}

\section{面向网络协同应用编程的编程框架的挑战}
\label{sec:nai-challenge}
为了解决传统套接字编程存在的“网络与应用割裂”问题，需要在终端节点的应用编程框架上增加相应的功能，经过对已有系统和研究的调研，发现目前主流的基于Socket的编程框架并不能很好地实现网络协同应用编程目标，具体来说，实现网络协同应用编程主要有以下挑战：

\textbf{C1: 无法区分不同传输需求的数据包。}实现网络协同应用编程的关键步骤之一是区分不同传输需求的数据包，然而，传统的基于Socket的编程模型很难实现该目标，应用层数据一旦进入了操作系统内核态网络栈，则丢失了所有应用相关上下文信息。以图~\ref{fig:nai-motivating-case} 中所示目标为例，以下讨论现有基于Socket的编程模型实现区分不同传输需求数据包的方法，并讨论其存在的问题。具体来说，传统基于Socket框架的应用往往有以下几种方案：

\begin{enumerate}
  \item 通过不同的目的IP地址区分不同传输需求数据包。通过目的主机的IP地址可以区分发往不同终端设备的数据包，然而，在图~\ref{fig:nai-motivating-case} 的案例中，由于Server只有一个IP地址，这意味着不同类型的数据包具有相同的目的IP地址，所以无法实现数据包区分目标。即使可以给Server分配多个IP地址，然而这样的方法极大的限制了伸缩性（Scalability）。
  \item 通过不同的源IP地址区分不同传输需求数据包。与基于目的IP地址区分数据包类似，在图~\ref{fig:nai-motivating-case} 的案例中，由于Client只有一个IP地址，无法实现数据包区分目标。
  \item 通过不同的目的端口号区分不同传输需求数据包。使用不同的目的端口号区分不同传输需求的数据包可以解决目的IP相同的问题，然而，该方案仍然无法解决图~\ref{fig:nai-motivating-case} 的目标。首先，Server的端口号往往是固定的，并通过固定端口接收所有目标为自己的数据包\footnote{例如HTTP2可以复用channel，使得同一个Socket连接承载不同传输需求数据包，由HTTP2作为表示层协议实现数据包区分}，导致Client端无法通过目的端口号区分不同传输需求数据包。比如在本文作者早期的云件（Cloudware）工作中，Server端只有一个WebSocket Server，通过应用层协议（如TLV格式）来判断不同类型的数据。即使允许Server绑定多个端口号，也会导致伸缩性和安全性问题，尤其对操作系统来说端口号为所有应用共享的紧缺资源（最多$2^{16}$个）。
  \item 通过不同的源端口号区分不同传输需求数据包。由于不同的Socket具有不同的源端口号，Client端可以创建多个Socket，不同传输需求数据发往不同的Socket实现区分。然而，该方案在实际情况下存在以下问题：首先，在目前的基于Socket网络编程框架中，源端口号一般为临时端口，由操作系统自动分配管理，在Socket连接结束后释放，无法将数据类型和源端口号进行映射；另一方面，如果使用指定的源端口号创建Socket，当应用存在$M$个数据类型和$N$个目标通信时（如反向代理），需要创建的Socket数量为$M\times N$，当数据类型和目标终端的数量庞大时，需要创建大量的Socket，在有限的源端口号资源情况下，系统的伸缩性将受到限制。
  \item 通过不同的物理网络接口区分不同传输需求数据包。除了上述方法外，基于Socket Intents\cite{schmidt2013socket}的方法可以利用不同的物理网络接口区分数据包，然而实际情况下对于发往相同接口的数据仍然无法区分其类型。如在\cref{fig:nai-motivating-case}的案例中，Client只有一个物理网络接口。
\end{enumerate}

综上所述，不同传输需求数据包的区分并不能简单地通过在应用层实现，造成该问题的根本原因在于传统的Socket编程模型失去了对内核态数据处理的控制。传统的Socket编程模型主要为了封装四层以下的网络编程，从而让应用程序能够专注于应用层逻辑，数据一旦进入内核态的网络协议栈，则丢失了上层逻辑中对数据类型的描述信息，导致无法对不同传输需求数据包进行区分。

\textbf{C2: 路径状态信息不可靠。}思路I2描述的方案是当前网络应用系统常用的方法，然而该方法存在测量不可靠。首先，传统的应用内测量路径方法往往通过TraceRoute工具实现，然而TraceRoute需要网络设备支持相应的功能和协议（如ICMP响应），且不同设备返回的信息不确定，比如可以配置为返回TraceRoute数据包的入口IP、返回TraceRoute数据包的返回接口IP、返回Loopback的IP地址。另外，对于路径的带宽和延迟测量也可能存在重复和冲突情况，考虑\cref{fig:nai-challenge-unsched}所示的例子，Client上部署有3个应用，这些应用都向Memcached集群写入数据，对于其中一个Memcahced服务器，三个应用都会测量Client到Memcached的路径状态，由于三个应用访问Memcached服务器具有相同的路径，多个应用将会重复测量该路径的状态，导致大量的带宽资源浪费。另一方面，当多个测量过程同时进行时，共享链路导致测量过程相互冲突，从而无法测得Client到Memcahced的真实带宽。在\cref{fig:nai-challenge-unsched}的例子中，不同应用可能测得如100Mbps、50Mbps或者33Mbps的路径状态，导致无法获得路径状态的基本事实（Ground truth）。


\begin{figure}[ht]
  \centering
  \includegraphics[width=0.9\linewidth]{figures/nai/challenge-unsched.pdf}
  \caption{路径状态测量重复和冲突示例}
  \label{fig:nai-challenge-unsched}
\end{figure}

% \textbf{C2：数据包头冲突。}
% 为了实现将不同类型的数据流与网络路径绑定，需要对不同类型数据流的数据包进行处理，使其能够通过不同网络路径转发。一种直接的方式是通过不同的数据包头来区分路径，然而，不同类型的数据流可能具有相同的数据包头。在图~\ref{fig:nai-motivating-case} 的案例中，由于Client和Server之间只有一个Socket，两种类型的数据流通过同一个Socket传输，导致进入网络的数据包具有相同的五元组，进而无法将属于不同类型数据流的数据包进行区分，本文将其称为\textbf{数据包头冲突挑战}。

% \textbf{C2: 数据发送前网络状态未知。}为了实现应用主动根据网络状态调整执行逻辑，比如设置合适的TCP buffer大小，需要主动获取网络状态并计算最优值。然而已有的被动调优方法需要根据历史数据传输情况将参数收敛到最优值，而本文的目标是通过事先获取网络状态，直接将参数设置到最优值，如何在未发送数据前获得路径状态是实现网络协同应用编程的挑战。

% \textbf{C3：循环依赖。}
% 为了解决网络或应用状态变化时自动触发AF和NF的重新执行问题，简单地使用观察者模式监听并触发函数重新执行可能会导致循环依赖问题。比如在图~\ref{fig:nai-motivating-case} 的案例中，AF和NF可以同时读写同一个状态变量$x$，当NF执行导致$x$发生变化，会触发AF重新执行，而AF的执行进一步导致$x$变化，进而又触发NF执行，最终形成循环依赖。

% \textbf{复杂依赖关系}: 将分离的AF和NF代码转换为一个紧密耦合的上下文。但原始代码中的所有复杂分支和条件使得难以检测到AF和NF之间的交互点。一个AF可能在不同的时间和情况下生成多个流，而一个NF可能有多个规则，这些规则可能在不同的事件上启用。静态分析哪些NF规则负责哪些AF流的复杂度很高。

% 为了解决这个问题，需要设计新的方法来精确地描述AF和NF之间的交互，并开发更高效的算法来处理这些交互，以提高系统的性能和可靠性。一种可能的解决方案是使用基于模型的方法，例如使用Petri网模型来描述AF和NF之间的交互，以及使用模型检测技术来自动化验证系统是否符合特定的属性。此外，还可以使用机器学习技术来分析系统行为并自动学习交互模式，以优化系统性能。

% \textbf{快速同步}: 在传统的编程模型中，大多数AF和NF独立运行在自己的上下文中，并定期从另一个上下文中拉取信息草图。当两次拉取之间发生突发变化时，它们无法及时反应以同步彼此的状态。


% 解决上述挑战并不能简单地通过修改应用程序本身实现，本文讨论将AF和NF绑定在终端主机时的设计。为了处理AF和NF的复杂依赖和快速同步，关键挑战是高效地处理通信。类似于进程间通信，AF和NF应该通过某些共享对象来交换信息。对于草案解决方案，本文有以下几种选择：

% \textbf{用户空间实现：}可以在用户空间创建共享对象作为全局变量。这种方法易于实现，用户空间也很灵活，能够支持各种使用场景。
% 然而，AF无法看到内核空间中的细节，例如它所生成的流的套接字特性和状态等底层网络细节。因此，NF无法通过共享对象唯一地处理这个流，因为AF没有将其网络特性写入共享对象中。

% \textbf{内核空间实现：} eBPF~\cite{bcc} 提供了一种特殊的数据结构，称为 eBPF 映射，它们在内核空间和用户空间之间是全局可访问的。通过 eBPF 映射，本文可以将 AF 与其在运行时调用的 socket 相关联，并将信息存储到 eBPF 映射中，以便 NF 可以检查并使用匹配的 socket 进行控制。然而，eBPF 映射实质上占用了有限的内核内存区域，而且映射类型受到内核的限制，此外，映射不支持发布/订阅 API 以执行自动重新执行。因此，这种解决方案也不适合本文的系统。

% 本文目标是为所有的 AF 和 NF 提供一个统一的框架，并在整个分布式系统中实现它们，包括网络节点，并为网络应用程序提供高级编程模型，以与网络控制交互。





% 本文注意到，AF和NF都与套接字进行交互。AF调用套接字发送/接收流，而NF设置套接字参数来表达控制逻辑。因此，本文提出了一个名为 Socker{} 的套接字级框架来解决这些挑战。为了解决AF和NF的\textbf{复杂依赖关系}，本文使用\textbf{动态流跟踪}来跟踪底层套接字，并动态绑定AF和NF。为了实现AF和NF之间的\textbf{快速同步}，本文采用\textbf{协议无关反应}来快速响应运行时的变化。在本文的初步评估设置中，结果显示，使用 Socker{} 构建的应用程序平均请求时间降低了28.5\%，相比传统基于套接字的应用程序。

\section{基于套接字跟踪的网络协同应用编程框架}
\label{sec:nai-solution}


% 为了解决\cref{sec:nai-challenge}中所提出的挑战，本文提出了面向网络协同应用编程的应用编程框架Socker。本节主要介绍Socker的编程模型和系统架构。

\subsection{可验证路由编程模型}
为了解决挑战C1（无法区分不同传输需求的数据包），本文提出了\textbf{S1: 基于套接字跟踪的网络协同应用编程框架}。造成挑战C1的根本原因在于\textbf{网络流的创建和网络路径配置之间相互独立，且应用层获取的网络信息受限于网络设备能力}。因此，本文利用验证系统提供的可靠信息作为应用获取网络状态的数据源之一，基于数据平面验证提供的可靠信息来建立应用编程，从而确保编程过程的准确性。具体而言，本文描述网络协同应用编程目标如下，给定一个网络$G(D,L)$，一个网络应用部署于终端设备$s \in D$，应用向网络中的某个目的终端设备发送不同类型的数据，需要注意的是数据类型的定义取决于应用程序，比如文件类型、实时消息类型等。令$TYPE=\{t_1,...,t_N\}$为应用发出的数据类型集合，对于每个数据类型$t_i \in TYPE$，应用程序指定其通过网络路径$\pi_{i}$进行传输，本文用$\Pi=\{\pi_1,...,\pi_N\}$表示所有应用指定的网络路径集合。
% 不同对于任意一个包头为$h_i$的数据包，应用程序指定该数据包通过网络路径$\pi_i$进行传输。需要注意的是，对于两个具有相同包头的数据包$h_i$和$h_j$，应用程序可以指定不同的$\pi_i \neq \pi_j$进行传输。

另一方面，应用程序逻辑和网络控制逻辑也可以读取应用和网络状态从而决定其执行策略，从而实现协同交互。本文将应用程序对数据的处理逻辑抽象为函数AF (Application Function)，其定义为$AF: TYPE \times DATA \times \Pi \rightarrow DATA$，其中$DATA=\{0,1\}^*$表示数据内容，一个AF输入一个数据的类型、数据内容以及网络路径，输出处理后的数据内容。同时，本文将网络控制逻辑抽象为函数NF（Network Function），其定义为$NF: \mathbb{G} \times TYPE \times D \rightarrow \Pi$，其中$\mathbb{G}$表示输入的网络拓扑域，一个NF输入一个网络拓扑、数据类型以及目的终端节点，输出一个从当前应用所在的终端设备到目的设备的网络路径。
% 其定义为$AF: TYPE \times term(D) \rightarrow \mathbb{H}$，其中$TYPE \subset \mathbb{N}$为数据流类型集合，本文用一个正整数表示一种类型编码，AF的语义为：对于发往目标$d \in term(D)$的类型为$s \in \mathbb{S}$的数据，发往网络的数据包头为$h \in \mathbb{H}$。另一方面，本文将网络控制逻辑抽象为函数NF（Network Function），其定义为$NF: SRC \times DST \rightarrow \Pi$，其中$SRC, DST \subseteq term(D)$, $\Pi$为网络中所有可能的路径集合。


Socker为实现AF和NF的协同编程以及可验证的路由状态查询，提供了两种网络状态查询接口：一种是声明式网络查询语言NQL，另一种是编程式网络查询接口。这样，应用开发人员可以灵活地选择适合的方式进行网络状态查询，确保编程的协同性和路由的正确性。


\begin{table}[h]
  \center
  \caption{NQL主要语法规则}
  \label{tab:nai-socker-api-grammar}
  \begin{tabular}{|rcl|}
    \hline
    $nql$ &:=& $target\ condition\ annotation* $\\
    $target$ &:=& QUERY $tname$\\
    $tname$ &:=& path | header\\
    $condition$ &:=& WITH $pred$\\
    $pred$ &:=& $expr$ | $pred$ pop $pred$\\
    $pop$ &:=& AND | OR | NOT\\
    $expr$ &:=& $id$ op $literal$ | $id$ op [$array$]\\
    $annotation$ &:=& ORDER BY $literal$ (ASC|DESC)? | LIMIT $literal$ | DISTINCT\\
    $op$ &:=& = | > | < | >= | <= | !=\\
    $id$ &:=& [a-zA-Z0-9]+\\
    $array$ &:=& $(literal ,)* literal$\\
    $literal$ &:=& ".*" | [0-9]+\\
    \hline
  \end{tabular}
\end{table}

\textbf{声明式网络查询语言NQL。}Socker提供了一套声明式的语言NQL（Network Query Language）来提供网络状态查询功能，系统将NQL编译成验证平面中对数据平面模型的图计算函数调用。如\cref{fig:nai-nql-example}所示为查询从H1或H2出发且包头目的IP为10.0.0.1的路径，返回的路径根据其跳数从小到大排序。\cref{tab:nai-socker-api-grammar}列出了NQL的主要语法规则，具体而言，NQL由三部分构成，即查询目标、查询条件和查询修饰符。

\begin{enumerate}
  \item 查询目标。查询目标通过关键字\textbf{QUERY}指定，用来说明NQL返回的结果集合类型，NQL支持路径（QUERY path）和包头（QUERY header）两种类型的查询目标。
  \item 查询条件。查询条件通过关键字\textbf{WITH}指定，后边跟一个由\textbf{AND}和\textbf{OR}构成的表达式谓词逻辑，如\cref{fig:nai-nql-example}中的WITH条件指定了路径必须是从H1或H2出发，且路径对应的包头目的IP为10.0.0.1。
  \item 查询修饰符。查询修饰符对返回的结果进行修饰，比如对结果集排序（ORDER BY），截取结果集（LIMIT），以及去除重复结果（DISTINCT），如\cref{fig:nai-nql-example}中的ORDER BY修饰符指定了返回的结果按照hopCount属性升序排序。
\end{enumerate}

\begin{figure}[h]
  \centering
  \includegraphics[width=\linewidth]{figures/nai/nql-example.pdf}
  \caption{NQL查询示例}
  \label{fig:nai-nql-example}
\end{figure}

\textbf{编程式网络查询接口。}除了声明式的NQL查询语言外，Socker还提供了编程式的网络查询接口，以实现自定义的网络查询过程。具体而言，Socker采用了函数式编程模型，通过自定义的Lambda匿名函数实现对结果集的处理。\cref{tab:nai-socker-api}列出了Socker提供的主要编程式网络查询接口，其中$T[]$表示类型为$T$的集合，$(*)$表示任意类型的输入。\cref{fig:nai-socker-api-example}给出了基于Socker编程式接口的代码示例，该示例中首先查询从SRCS到DSTS的所有路径，然后筛选跳数小于10的路径，之后对各路径计算传输大小为dataSize的数据需要的时间，最后按照传输时间从小到大排序并将结果存在pathSet变量中。

\begin{table}[h]
  \center
  \caption{Socker编程式网络查询接口}
  \label{tab:nai-socker-api}
  \begin{tabular}{|c|c|c|c|c|c|c|}
    \hline
    \emph{函数名称}
    & \emph{输入输出类型}
    & \emph{Lambda函数}
    & \emph{说明}
    \\\hline
    $map(func)$ & $T[] \Rightarrow U[]$ & $func: T \Rightarrow U$ & 对结果集所有元素做变换
    \\\hline
    $flatMap(func)$ & $T[] \Rightarrow U[]$ & $func: T \Rightarrow U[]$ & 所有元素变换为集合后合并
    \\\hline
    $filter(func)$ & $T[] \Rightarrow T[]$ & $func: T \Rightarrow Bool$ & 筛选结果集元素
    \\\hline
    $sort(func)$ & $T[] \Rightarrow T[]$ & $func: T \Rightarrow Bool$ & 对结果集排序
    \\\hline
    $group(func)$ & $T[] \Rightarrow (U, T[])[]$ & $func: T \Rightarrow Int$ & 按func指定的索引位置为键划分
    \\\hline
    $reduce(func)$ & $T[] \Rightarrow T[]$ & $func: (T, T) \Rightarrow T$ & 对所有元素聚合
    \\\hline
    $union(func)$ & $T[] \Rightarrow T[]$ & $func: (*) \Rightarrow T[]$ & 合并两个结果集
    \\\hline
    $count()$ & $T[] \Rightarrow Int$ & $void$ & 获取集合元素数量
    \\\hline
    $verify(func)$ & $T[] \Rightarrow T[]$ & $func: T \Rightarrow Bool$ & 验证并返回通过的集合
    \\\hline
  \end{tabular}
\end{table}


\begin{figure}[t]
  \begin{python}
import {model} from 'socker';

const pathSet = model.paths.query({sources: SRCS, targets: DSTS})
  .filter(path => path.hopCount < 10)
  .map(path => ({
    ...path,
    time: dataSize / path.throughput + path.delay # data transfer time (s)
  }))
  .sort((obj1, obj2) => obj1.time > obj2.time);
  \end{python}
  \caption{Socker编程式网络查询接口代码示例}
  \label{fig:nai-socker-api-example}
\end{figure}

% 对于AF，用户空间的共享对象可以存储应用程序级别的信息；而eBPF映射可以存储网络级别的信息，由于只需记录应用程序和套接字匹配关系的键值对，因此相对较小。

% \subsection{系统设计目标}
% \begin{figure}
%   \centering
%   \includegraphics[width=0.7\linewidth]{figures/nai/flow-bind.pdf}
%   \caption{数据和路径的绑定映射到数据包和路径包头的绑定}
%   \label{fig:abs-model}
% \end{figure}


在AF和NF的交互层面，为了实现AF和NF的协同编程，Socker采用了一种与以往所有工作不同的方案：将数据流创建的AF和网络控制的NF置于位于终端主机的同一编程上下文中。这样，应用程序和网络控制可以直接交换信息。同时，由于它们位于端点主机的同一上下文中，从而避免了应用程序泄漏重要信息的安全问题。

Socker的套接字编程是在传统的Socket编程模型上做了封装和扩展，与传统Socket编程模型不同，Socker的套接字一方面用来定义数据传输相关元数据（如目的IP，端口号等），另一方面实现AF和NF的绑定。具体来说，程序员通过AF函数来描述数据处理逻辑，通过NF函数描述网络控制逻辑，在创建Socket时候将AF和NF进行绑定，对于发往该socket的数据，系统将采用AF所定义的方式对数据进行处理，并通过NF所定义的路径对数据包进行路由。如图~\ref{fig:socker-programming-model} 所示为~\cref{fig:nai-motivating-case} 所示案例的Socker程序\footnote{为了精简表达核心过程，代码忽略了部分语法和函数规则}。其中AF函数根据数据类型和网络路径的带宽决定是否需要对数据进行压缩，NF函数则根据数据类型计算相应的网络路径，最后在创建Socker实例时将AF和NF进行绑定。对于NF所指定的路由，AF可以通过Socker的验证接口（verify函数）实现对路由的验证，从而实现灵活的可验证路由编程过程。

需要注意的是，套接字最终创建的数据流只能在运行时确定（例如，socket()参数为变量或临时端口分配）。同时，NF的执行结果在运行前也是未知的。在高级抽象层面上，Socker 实现了从编程时的数据和路径绑定到运行时的数据包和路径包头绑定。在用户态层面，AF负责对应用数据进行处理，并通过数据类型作为桥梁与NF的路径绑定。在内核态，Socker通过跟踪套接字创建过程进而获得不同类型数据在内核态的数据包，在将数据包发送到网络前，对数据包进行一定的转换操作（如NAT/隧道）使数据包具备通过特定路径转发的包头（路径包头），从而实现不同类型数据产生的数据包通过NF指定的路径进行传输。而路径包头（path.header）由数据平面配置生成过程决定，具体在\cref{cha:dpg}中进行阐述。为了实现内核态套接字的跟踪，Socker需要能够在内核态捕获并重写套接字相关的系统调用，比如使用覆写libc库将系统调用重定向到额外的内核模块，或者使用独立的内核模块驱动进而通过独立硬件（如Smart NICs）实现数据包区分。然而，eBPF提供了完善的函数钩子函数接口和数据包处理功能，所以，eBPF并非实现Socker的唯一途径，但在当前环境下是实现Socker的最佳软件框架。
% 另外，Socker的设计还考虑了以下方面：

% \begin{enumerate}
%   \item \textbf{AF和NF之间的通信协议无关。} 在设计AF和NF之间的通信机制时，最自然方式是通过直接函数调用实现耦合。然而，从AF和NF之间的协议角度来看，这会导致AF和NF之间的耦合性很强，任何参数的更改都会导致调用方的很多变化。此外，当AF/NF内部的某个变量状态发生变化时，实现AF和NF的自动重新执行也很困难。因此，Socker采用可观察设计模式来处理AF和NF之间的通信，即将共享对象存储在一个独立的程序级数据总线中，该总线配备了发布/订阅API。
%   \item \textbf{在运行时在内核中跟踪套接字创建的流以获取临时端口。} 以创建 TCP 流为例，在调用 \textit{connect()} 系统调用之前，套接字的本地端口号尚未确定。然而，\textit{connect()} 系统调用会导致内核中的 TCP 三次握手，因此在 \textit{connect()} 系统调用返回后已经发送了一组数据包，导致握手数据包无法控制。因此，必须在内核空间中跟踪套接字的流，为此，Socker选择使用 eBPF 工具集来跟踪内核中的系统调用。
% \end{enumerate}


\begin{figure}[t]
  \begin{python}
    from socker import topo, model, socker

    def AF(type, data, path):
      path = model.paths.query({sources: path.src, header: path.header})
        .verify(path => Regex.match(path, path.src+".*"+path.dst)).first()
      if path == None:
        ALERT("Verification error!")
      else if path.throughput < 10 and type == FILETYPE:
        data = compress(data)
      return data

    def NF(topo, type, target):
      if type == FILETYPE:
        return maxBWPath(topo, dst=target)
      else:
        return shortestPath(topo, dst=target)

    addr = (SERVER, PORT)
    so = socker(addr, af=AF, nf=NF)

    def send_realtime(data):
      so.send(RTTYPE, data)
    
    def send_file(data)
      so.send(FILETYPE, data)
  \end{python}
  \caption{Socker网络协同应用编程代码示例}
  \label{fig:socker-programming-model}
\end{figure}




\subsection{基于套接字跟踪的数据流区分设计}
为了解决数据流的不确定性挑战，需要跟踪应用程序的数据流创建过程，而数据流的创建主要由socket系统调用实现。选择套接字层作为跟踪的目标有以下原因，首先，观察到套接字是应用程序和网络之间的关键接口，套接字在libc提供的长期测试和稳定的API支持下，可以实现语言无关的实现并实现全局管理。此外，新兴的eBPF和内核追踪技术打开了系统调用的黑盒，并且可以根据套接字的属性轻松实现高性能的数据包处理。


如\cref{fig:nai-socker-arch}所示为Socker的系统架构，其中蓝色部分由用户提供，绿色部分为Socker提供的功能模块，红色部分由操作系统提供。图中用绿色箭头表示Socker的控制路径，蓝色箭头表示Socker的数据路径。在操作系统的用户空间，Socker包括三个模块，其中Socket map存储NF所指定的路径，Socket manager根据Socket map中的路径创建Socket套接字，并返回对应的套接字文件描述符，Socket map存储Socket manager创建的套接字文件描述符和路径的映射关系。当数据通过AF发送到Socker时，Data distributor根据Socket map中的信息将数据发送到对应的套接字。在操作系统内核空间，Socker维护一系列eBPF maps，当数据从内核态套接字发往TC（Traffic control）时候，TC所绑定的eBPF程序根据eBPF maps中的信息对数据进行处理，最终通过物理端口发往网络。

\begin{figure}[t]
  \centering
  \includegraphics[width=0.73\linewidth]{figures/nai/socker-arch.pdf}
  \caption{Socker系统架构}
  \label{fig:nai-socker-arch}
\end{figure}

首先，为了跟踪操作系统所创建的套接字信息，Socker基于eBPF实现内核态套接字跟踪。Socker 的 \textbf{eBPF Map} 主要目的是将由用户空间套接字创建的流与路径（NF的执行结果）进行映射。然而，没有一种直接的方式可以将它们链接起来（例如，用户空间无法直接获取内核套接字结构的地址）。为此，Socker设计了四种基本的 BPF\_MAP\_TYPE\_HASH 类型的 eBPF map，如表~\ref{tab:ebpf-maps} 所示，并使用内核套接字结构的 inode 编号作为用户空间套接字和内核空间套接字之间的桥梁，因为用户空间程序可以在 Linux \textit{proc} 文件系统上读取文件链接，并获取相应的内核套接字 inode 编号。

\begin{table}
  \caption{Socker中主要的eBPF maps}
  \label{tab:ebpf-maps}
  \begin{tabularx}{\linewidth} {|c|c|c|X|}
   \hline
   Map 名称 & 键类型 & 值类型 & 描述 \\
   \hline
   pids  & u32  & u32 & 存储eBPF代码的用户空间进程ID集，以筛选需要控制的套接字。由于内核跟踪系统中的kprobe拦截来自所有用户空间进程的系统调用，因此它还可以通过过滤进程来提高性能。 \\
   \hline 
   sk2ino & struct sock* & u64 & 包含从内核套接字结构的指针到内核套接字文件描述符的inode的映射。 \\
   \hline
   hash2ino & u32 & u64 & 存储从5元组的散列到套接字inode编号的映射。 \\
   \hline
   ino2route & u64 & struct route & 存储从内核套接字索引节点编号到NF计算结果的映射。 \\
   \hline
  \end{tabularx}
\end{table}

\textbf{Socker的套接字跟踪流程。}Socker跟踪了套接字创建过程中的以下系统调用函数，并将结果存储在eBPF maps中。Socker 包含一组附加到系统调用的 eBPF 程序，用于跟踪应用程序创建的套接字和 TCP/UDP 连接，此外，Socker还将一个 eBPF 程序附加到 TC 出口，以执行数据包控制。

\begin{itemize}
  \item \textbf{kprobe:sock\_alloc\_file.} 当创建套接字系统调用被调用时，内核将构建一个与文件结构相关联的内核套接字结构。Socker将一个kprobe eBPF函数附加到sock\_alloc\_file上，以获取内核套接字结构和其inode编号的映射，并将映射存储到sk2ino映射表中。当socket(2)系统调用返回到用户程序时，libsocker会从文件系统目录(\textit{/proc/pid/fs/fd})查询inode编号，以获取内核套接字inode编号，然后执行NF以获取所需路由，并将inode到路由的映射存储到ino2route映射表中。
  \item \textbf{kprobe:tcp\_connect.} Socker将一个kprobe eBPF程序附加到内核的tcp\_connect函数上。Socker在此处创建钩子，因为当tcp\_connect函数被调用时，套接字的本地端口已经确定，但是TCP SYN包尚未被发出。这使Socker能够控制SYN包，Socker计算5-tuple的哈希值，并查找sk2ino以获取当前套接字的inode编号，然后将映射存储到hash2ino映射表中。
  \item \textbf{SCHED\_CLS:tc\_egress.} Socker将eBPF程序附加到TC出口，程序从\textit{\_\_sk\_buff}计算5-tuple的哈希值，并查找hash2ino以获取当前套接字的内核套接字inode编号，然后查找ino2route映射表以获取当前数据包的期望路由。在发往网络之前，数据包将被适当地处理以适应网络基础设施(例如，在MPLS网络中使用MPLS堆栈封装数据包)。
\end{itemize}

\begin{figure}[t]
  \centering
  \includegraphics[width=\linewidth]{figures/nai/socker-workflow.pdf}
  \caption{基于Socker的TCP工作流程}
  \label{fig:socker-workflow}
\end{figure}

为了说明 Socker 如何实现应用程序和网络的联合控制，以 TCP 连接工作流程为例来描述该过程，如图~\ref{fig:socker-workflow} 所示，TCP socket 的生命周期被分为四个阶段。对于不调用 \textit{connect()} 系统调用的 UDP socket，Socker在 UDP socket 的 \textit{send()} 系统调用中执行类似于 TCP \textit{connect()} 系统调用的过程。


\textbf{创建套接字阶段。} 应用程序调用libsocker中的\textit{socker()}函数来创建一个套接字，并将网络函数指针作为其中的一个参数。libsocker将当前进程的ID写入\textit{pids} eBPF映射，并调用NF以获取结果。同时，libsocker调用socket系统调用，跟踪内核函数\textit{sock\_alloc\_file}的执行，并将内核套接字结构和表示套接字文件描述符的inode号之间的映射存储起来。在socket系统调用返回后，libsocker查询用户空间套接字文件描述符的链接信息，以获取内核套接字的inode号。最后，libsocker将inode号与NF的结果的映射存储到\textit{ino2route} eBPF映射中，并返回给应用程序。

\textbf{连接阶段。} 当应用程序发出connect系统调用时，跟踪\textit{tcp\_connect()}内核函数在SYN包发送之前的执行。eBPF程序查找\textit{sk2ino} eBPF映射以获取内核套接字inode编号，并计算5元组的哈希值，然后将从哈希到inode编号的映射存储到\textit{hash2ino} eBPF映射中。对于UDP套接字，上述过程移动到了发送阶段。

\textbf{发送阶段。} 在TCP连接建立后，应用程序通过套接字写入系统调用（例如\textit{send}/\textit{sendto()}）向套接字发送数据包。附加到流量控制（TC）出口的eBPF程序在将数据包发射到网络接口之前获取套接字缓冲区。当数据包到达TC出口时，eBPF程序从套接字缓冲区计算5元组的哈希值，并查找\textit{hash2ino}映射以获取当前套接字的内核套接字inode号码，然后查找\textit{ino2route}以获取当前数据包的所需路由。最后，在将数据包发射到网络接口之前，将其重写（例如用MPLS封装）。

\textbf{关闭阶段。} 当应用程序显式关闭套接字或由于错误关闭套接字时，Socker将清理所有相关的eBPF映射中的条目，关闭内核空间的Socket连接，并清理所有相关的用户空间和内核空间的套接字。

\begin{figure}
  \centering
  \includegraphics[width=\linewidth]{figures/nai/socker-overall.pdf}
  \caption{Socker系统的控制平面和数据平面结构}
  \label{fig:socker-overall}
\end{figure}

Socker提供了一套与传统Socket兼容的支持网络协同应用编程接口，在传统Socket接口上扩展了网络逻辑函数绑定，其总体架构包括控制平面和数据平面，如图~\ref{fig:socker-overall}所示。红色箭头表示控制路径，蓝色箭头表示数据路径。Socker的控制平面主要负责三个任务：为AF和NF提供共享数据层，并自动触发重新执行，跟踪应用程序的上下文信息（例如，用户空间的套接字），以及将期望的网络状态（例如，流的期望路径）同步到数据平面。


这些任务通过一个名为\textit{network-application shared data bus}（NAB）的数据总线、一个编程语言相关的高级编程库libsocker，以及一组eBPF程序实现。为了实现AF和NF之间的交互以及在状态更改时实现自动重新执行，将NAB设计为数据总线，因此AF和NF可以以协议无关的方式共享它们的状态。此外，NAB采用可观察设计模式，具有发布/订阅API，自动将AF/NF注册为其相关状态的观察者，因此对状态的任何更改都可以触发AF/NF的重新执行。网络状态由Socker从控制器或者网络可见性框架（见~\cref{sec:nai-solution-vis}）中拉取并发布到NAB中。libsocker封装了原始的用户级套接字函数和一组附加到系统调用的eBPF程序。应用程序使用libsocker创建套接字，因此可以通过Socker捕获进程运行时信息（例如进程ID、套接字文件描述符）。另外，eBPF程序跟踪与应用程序创建的套接字相关的内核函数（例如sock\_alloc\_file(2)/tcp\_connect(2)），并将跟踪的状态存储在内核eBPF映射中。eBPF程序附加到套接字系统调用上，跟踪传递给系统调用的参数，并将信息存储到eBPF映射中。此外，NF与网络策略之间的同步需要一定的机制，以允许主机将网络配置注入到数据路径中。例如，NF使用生成的路径与数据平面配置生成系统如Fusion（见\cref{cha:dpg}）通信来获取路径对应的包头。

Socker 的数据平面负责将 NF 返回的所需操作应用到出站数据包上，通过将一个 eBPF 程序附加到出口流量控制（TC）上完成。例如，在源路由网络中，如果 NF 是一个路由控制功能并返回流的路径，那么 eBPF 将在将数据包发出到网络接口之前对流的数据包进行封装。或者路径可以对应于一个标签，eBPF可以通过NAT方式对包头进行修改从而实现路径绑定。该过程涉及到数据平面配置所使用的生成过程和协议，具体将在\cref{cha:dpg}中介绍。




\section{基于ALTO和perfSONAR的路径可见性框架}
\label{sec:nai-solution-vis}
为了解决挑战C2，本文设计了\textbf{S2:基于ALTO和perfSONAR的路径可见性框架}。造成C2所描述问题的根本原因在于测量模块之间缺乏统一协调，网络系统有必要将测量功能单独提出来作为共享支撑系统，Socker程序通过统一的接口查询路径状态（如带宽、延迟等），以支持网络状态相关的应用程序实现传输层调优。

ALTO (Application-Layer Traffic Optimization) ~\cite{shalunov2014applicationlayer}是一种用于优化网络传输的应用层协议。它旨在帮助应用程序更有效地利用网络资源，提高网络传输的性能和效率。ALTO提供了一种机制，使应用程序可以获取有关网络拓扑、链路状况和网络资源分布的信息，从而更好地决策数据传输路径和策略。ALTO的主要功能包括：
\begin{enumerate}
  \item Network map：提供网络拓扑和连接信息，帮助应用程序了解网络结构和链路状况。
  \item Cost map：提供网络中各个终端节点（endpoints）之间的网络情况，如带宽、延迟和丢包情况等。
\end{enumerate}

ALTO采用客户端-服务器模型，应用程序可以通过Restful格式向ALTO服务器发送查询请求，获取网络信息和传输建议。ALTO服务器则根据网络拓扑和资源信息，计算出最优的传输策略，并将结果返回给应用程序。ALTO的设计旨在降低网络传输的成本，提高传输质量，优化网络资源利用，从而为应用程序提供更好的用户体验。它在多种应用场景中得到应用，如P2P文件共享、流媒体、CDN（内容分发网络）优化等。总的来说，ALTO是一种通过提供网络信息和传输优化建议，帮助应用程序实现更高效传输的应用层协议。

perfSONAR（Performance Service-Oriented Network monitoring ARchitecture）~\cite{hanemann2005perfsonar}是一个开源的网络性能监测和测量架构。它旨在提供全球范围内的网络性能监测服务，以帮助网络管理员和研究人员诊断和解决网络性能问题。perfSONAR提供了一系列用于测量网络性能的工具和服务，包括带宽测量、时延测量、数据包丢失测量等。这些工具和服务可以在全球范围内的网络节点部署，以便进行网络性能的实时监测和分析。perfSONAR架构采用分布式和服务导向的设计理念，允许用户通过网络查询和收集性能数据。它还提供了丰富的数据可视化和分析功能，使用户能够更好地理解网络性能和趋势，并发现潜在的性能瓶颈和故障点。perfSONAR的使用范围广泛，涵盖了科研机构、大学、互联网服务提供商（ISP）、科学研究网络（如Internet2和GEANT）等领域。它对于实时监测和优化大规模网络的性能非常有价值，尤其在支持高性能计算、大规模数据传输、视频流服务和云计算等应用方面发挥着重要作用。总的来说，perfSONAR是一个用于网络性能监测和测量的开源架构，它为用户提供了一系列工具和服务，帮助实时监测和分析网络性能，以提高网络的可用性和性能。

\begin{figure}
  \centering
  \includegraphics[width=0.70\linewidth]{figures/nai/alto.pdf}
  \caption{基于ALTO和perfSONAR的网络可见性框架结构}
  \label{fig:nai-alto}
\end{figure}

如\cref{fig:nai-alto}所示为基于ALTO和perfSONAR的网络可见性框架结构，系统在终端设备上分布式地部署多个perfSONAR实例，并配置其测量实例之间的路径状态，主要包括以下指标：路径带宽（Bandwidth）、单向时延（One-way delay），双向时延（Round-trip delay）。perfSONAR的测量数据被收集到一个集中的数据仓库perfSONAR Archive DB，ALTO server以该数据仓库作为数据源计算终端节点之间的cost metrics，最后向ALTO client提供查询服务。

然而，上述结构仍然存在以下潜在的问题：1）应用部署的终端设备可能并未部署perfSONAR实例，导致丢失了某些终端设备之间的路径状态信息。2）perfSONAR的测量过程可能与应用传输同时发生，导致无法获得稳定的测量值。为了解决上述问题，需要对ALTO进行了扩展，并提供了相应的近似算法，具体而言，系统在ALTO协议的cost-type部分增加了以下两个字段：
\begin{enumerate}
  \item 近似算法（anchor-alg）：用户可以指定用何种方式找到相应的perfSONAR实例从而获得近似的测量结果，包括同管理域（same-site）、最近位置（closest）和IP最长匹配（lpm）三种近似方式。
  \item 时间区间（interval）：用户可以指定利用某个时间段的测量数据生成最终结果，比如最近的10分钟为last: 10m。
\end{enumerate}

另外，原本的ALTO协议endpoints域为一个只包括srcs和dsts字段的对象，这种结构会导致冗余的结果，比如希望同时获得$A$到$B$和$C$，以及$B$到$C$和$D$的测量值，原本的ALTO协议会产生$|\{A,B\}| \times |\{B,C,D\}| = 6$条结果，为此，系统将ALTO的endpoints字段进一步扩展为一个数组，数组中每个元素为包含srcs和dsts的对象，从而只返回需要的$|\{A\}| \times |\{B,C\}| + |\{B\}| \times |\{C,D\}| = 4$条结果。

如\cref{fig:nai-alto-req-resp}所示为一个基于perfSONAR的ALTO Server请求和响应示例，该例子请求从IP地址为202.122.33.13的终端节点到IP地址为192.41.231.82的终端节点的网络带宽，带宽值的计算选取与两个终端节点在同一个网络管理域的perfSONAR实例最近10分钟的测量结果为数据源，并返回平均带宽值（tput:mean）。Socker系统通过NQL或编程式接口请求路由信息时，系统通过ALTO接口请求路径对应的状态信息，并作为路径属性传递给上层应用。

\begin{figure}[ht]
  \centering
  \subcaptionbox{ALTO Server请求示例\label{fig:nai-alto-req}}{
    \includegraphics[width=0.48\textwidth]{figures/nai/alto-req.png}}
  \subcaptionbox{ALTO Server响应示例\label{fig:nai-alto-resp}}{
    \includegraphics[width=0.48\textwidth]{figures/nai/alto-resp.png}}
  \caption{基于ALTO和perfSONAR的路径可见性系统请求和响应示例}
  \label{fig:nai-alto-req-resp}
\end{figure}

% \begin{figure}[t]
%   \begin{python}
%     # 请求
%     {
%       "cost-type": {
%         "cost-metric": "tput:mean",
%         "anchor-alg": "closest",
%         "interval": {
%           "last": "10m"
%         }
%       },
%       "endpoints": [{
%         "srcs": [
%           "ipv4:202.122.33.13"
%         ],
%         "dsts":[
%           "ipv4:192.41.231.82"
%         ]
%       }]
%     }
%     # 响应
%     {
%       "meta": {
%         "cost-type": {
%           "cost-metric": "tput:mean",
%           "anchor-alg": "closest",
%           "interval": {
%             "last": "10m"
%           }
%         }
%       },
%       "endpoint-cost-map": {
%         "ipv4:202.122.33.13": {
%           "ipv4: 192.41.231.82": 203917004.25
%         }
%       }
%     }
%   \end{python}
%   \caption{基于perfSONAR的ALTO Server请求和响应示例}
%   \label{fig:nai-alto-req}
% \end{figure}


\section{实验设计和结果分析}
\label{sec:nai-eval}
本节通过使用对 Socker 在进行评估，目标是评估使用 Socker 进行套接字编程相对于传统套接字编程的优势。具体而言，本章通过实验回答以下问题：
\begin{enumerate}
  \item Socker是否能够实现应用传输需求与路径的适配？
  % \item Socker的时间和内存开销多大？
  \item 基于ALTO和perfSONAR的网络路径可见性框架是否能够实现更优的传输控制？
\end{enumerate}

\subsection{系统实现}
Socker基于BCC~\cite{bcc}（v0.20.0）库实现了原型系统~\footnote{https://github.com/guodong/socker}。并针对应用程序API，实现了一个Python库libsocker。NAB是通过一个遵循观察者设计模式的Python类来实现的，通过Python函数注解来暴露订阅API，当订阅的数据发生变化时，会触发封装的函数，并将数据作为参数传递给封装的函数。网络可见性框架基于perfSONAR的网络测量组件iPerf3~\cite{iperf3}实现，并默认配置测量间隔为10分钟。

\subsection{Socker实现传输需求与路径适配的效果评估}

\begin{figure}
  \centering
  \includegraphics[width=0.8\linewidth]{figures/nai/eval-topo.pdf}
  \caption{实验使用的拓扑}
  \label{fig:nai-eval-topo}
\end{figure}



\textbf{实验设计。}实验环境在装有启用了eBPF内核模块的Linux内核5.8.0的Ubuntu20.04.1台式机上进行，该台式机配备有Intel i7 CPU（2.2GHz）和16GB内存。实验使用mininet~\cite{lantz2010network}构建了一个网络，如图~\ref{fig:nai-eval-topo}所示，其中包括三个交换机和两个主机。服务器运行一个TCP服务器以接收数据，客户端使用图~\ref{fig:socker-programming-model}中的Socker程序运行。实验使用Large Text Compression Benchmark~\cite{ltcb}的\textit{enwik8}（100MB文本文件）数据集作为程序的输入。网络链接分配的带宽在图中标明，其中默认路径为Client-S1-S2-Server。实验在该拓扑上进行了多次评估，每次设置链路S1-S2的带宽为不同值（50-150Mbps），并获取应用程序传输数据所需要的时间。为了和Socker对比，编写了另一个基于传统套接字的数据传输程序，以执行与Socker程序中相同的任务，并对比其数据传输时间。

\textbf{实验结果。}
\cref{fig:socker-eval-time}所示为S1-S2在不同带宽下的Socket和Socker传输时间比较，在带宽介于50mbps到90mbps之间时，可以观察到Socker的传输时间明显短于Socket的传输时间。这是因为Socker选择了最大带宽路径进行传输，而Socket只能使用默认路径进行传输。当S1-S2的带宽大于等于100mbps时，Socker和Socket选择了相同的传输路径，因此两者的传输时间基本相同。在该实验环境中，Socker平均传输时间较Socket降低了20\%，所以，得出结论：\textbf{相比传统Socket框架，Socker能够适应应用需求，提供更优的数据传输}。

\begin{figure}
  \centering
  \includegraphics[width=0.8\linewidth]{figures/nai/eval-time.pdf}
  \caption{不同带宽下的Socket和Socker传输时间比较}
  \label{fig:socker-eval-time}
\end{figure}

% \begin{figure}
%   \centering
%   \includegraphics[width=\linewidth]{figures/no-socker.png}
%   \caption{The compression and network transmission time under different compress levels.}
%   \label{fig:eval-no-socker}
% \end{figure}

% 结果如图~\ref{fig:eval-no-socker} 所示。本文可以看到，在不同的压缩级别下，Socker 总是优于普通 socket 程序，并平均实现了超过 28.5\% 的上传时间缩短。

% 本文之所以能够获得这种收益，是因为 Socker 允许应用程序为典型流量选择更好的路由。在本文的案例中，大文件上传流使用具有较高带宽（稍长的延迟）的路由（即 maxBWPath），而不是默认路由（即 shortestPath）。此外，应用程序可以根据路由的带宽和 QoS 要求调整其压缩级别，这是图~\ref{lst:eval-program} 中的 \textit{computeCL} 函数的任务。 \textit{computeCL} 函数的设计超出了本文的研究范围，但是本文评估了 Gzip 的所有可能压缩级别（从 1-9），结果显示 Socker 在所有情况下都能够获得收益。

Socker对Socket的功能扩展势必会带来额外的时间和资源开销，所以本文设计了以下实验来评估Socker的时间和内存开销，具体实验如下：

% \subsection{Socker的资源开销评估}
\textbf{实验设计。}
为了评估Socker的时间和内存开销，实验对Socket和Socker程序在使用相同路径的情况下进行比较，以获得它们的数据传输时间和内存占用情况。选择相同路径是为了确保数据传输时间不会受到路径带宽的影响。

\textbf{实验结果。}
如\cref{fig:nai-overhead}所示为Socker的时间和内存开销实验结果，\cref{fig:nai-overhead-mem}为Socker与Socket相比的时间开销，可以看到Socker和Socket之间的数据传输时间几乎相同，仅有极少量的增加，平均增加仅为0.5\%。造成Socker传输时间更长的原因在于Socker具有更复杂的内核数据包处理过程。\cref{fig:nai-overhead-mem}为Socker与Socket比较的内存开销，平均增加9\%的内存占用，造成Socker内存占用更大的原因在于Socker需要存储网络相关信息（如拓扑）以及相关图算法库（如Networkx）。通过该实验，得出以下结论：\textbf{与传统的Socket框架相比，Socker仅引起微小的时间增加和轻微的内存占用增加。}

\begin{figure} %[h]
  \centering%
  \subcaptionbox{Socker的时间开销}{\label{fig:nai-overhead-time}
    \includegraphics[width=0.4\textwidth]{figures/nai/eval-time-overhead.pdf}}
  \subcaptionbox{Socker的内存开销}{\label{fig:nai-overhead-mem} 
    \includegraphics[width=0.4\textwidth]{figures/nai/eval-mem-overhead.pdf}}
  \caption{Socker资源开销实验结果}
  \label{fig:nai-overhead}
\end{figure}

\subsection{基于ALTO和perfSONAR的可见性框架效果评估}
\textbf{实验设计。}
实验基于\cref{fig:nai-eval-topo}进行了如下评估。首先，在Client和Server端分别部署了perfSONAR的带宽和延迟测量组件iPerf3，并将Client到Server的实时带宽($B$)和延迟($D$)信息存入NAB数据库，Socker程序通过获取带宽和延迟信息，在创建Socker实例时传入TCP缓冲区大小$S$，其中$S$的计算根据带宽延迟乘积获得$S = B \times D$。然后，Socker发送100MB的文件到Server，并获取文件传输的时间。同时，实验与传统套接字程序直接发送文件所需要的时间进行比较，Socket程序采用操作系统默认的配置。实验分别设置链路S1-S2的带宽（100-150mbps）和时延（20-100ms），并分别执行Socker和Socket程序。

\begin{figure} %[h]
  \centering%
  \subcaptionbox{S1-S2带宽100mbps}{
    \includegraphics[width=0.32\textwidth]{figures/nai/eval-alto-time100.pdf}}
  \subcaptionbox{S1-S2带宽110mbps}{
    \includegraphics[width=0.32\textwidth]{figures/nai/eval-alto-time110.pdf}}
  \subcaptionbox{S1-S2带宽120mbps}{
    \includegraphics[width=0.32\textwidth]{figures/nai/eval-alto-time120.pdf}}
  \vskip0.5cm
  \subcaptionbox{S1-S2带宽130mbps}{
    \includegraphics[width=0.32\textwidth]{figures/nai/eval-alto-time130.pdf}}
  \subcaptionbox{S1-S2带宽140mbps}{
    \includegraphics[width=0.32\textwidth]{figures/nai/eval-alto-time140.pdf}}
  \subcaptionbox{S1-S2带宽150mbps}{
    \includegraphics[width=0.32\textwidth]{figures/nai/eval-alto-time150.pdf}}
  \caption{基于ALTO和perfSONAR的可见性框架对Socker的效果}
  \label{fig:nai-eval-alto}
\end{figure}

\textbf{实验结果。}
实验结果展示在\cref{fig:nai-eval-alto}中。可以明显观察到，在各种实验设置下，Socker的传输时间都明显低于Socket的传输时间，平均缩短了13\%。主要原因是因为Socker能够在数据发送前设置与网络状态一致的Socket缓冲区，而传统TCP Socket需要在发送过程中被动地调整缓冲区大小，直到收敛于合适的值。


\section{本章小结}
本章的重点在于研究网络协同应用编程框架，实现可验证路由编程系统，提升网络的适应性。本章首先深入分析了当前网络应用编程中的“网络与应用割裂”现象，随后探讨了实现网络协同应用编程所必须应对的数据包区分和路径可见性挑战。为了克服这些挑战，引入了创新性的网络协同应用编程框架Socker，提出了基于声明式和编程式的可靠路由查询接口，并基于Socket跟踪技术实现传输需求与路径的适配，Socker融合了ALTO和perfSONAR等网络可见性框架的功能。实验表明，相比传统套接字编程，Socker能够实现20\%的传输优化，并只有微小的时间和内存开销。